package model;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import javax.swing.Timer;

import common.DataManager;
import common.GridCoordinate;
import common.ItemFactory;
import common.MessageFactory;

import controller.Controller;

/**
 * @author Colin
 * 
 * The Map class is responsible for creating the map in the model,
 * laying Items on the map and controlling contact between zombosaurs
 * and PlayableItems.
 *
 */
public class Map{
	
	static final int MAP_WIDTH = 15;
	static final int MAP_HEIGHT = 10;
	static final int KILL_ZOMBOSAUR_POINTS = 10;
	
	private int tickCount = 0;
	private float timeBeforeSpawn = 2.0f;
	
	private static ArrayList<ArrayList<Tile>> mapGrid;	//accessed by row(col)
	private ArrayList<TrailTile> trail;
	
	private ArrayList<AutomaticWeapon> automaticWeapons;
	private ArrayList<DefensiveItem> defensiveItems;
	//we need a separate list for mines, since they work differently than other defensive items. 
	//Instead of damaging each zombosaur that comes into contact with them like the other defensive items do, they have an area effect.
	private ArrayList<Mine> minesOnMap;
	
	private ArrayList<Ammo> ammoOnMap; //ManualWeapon ammo
	private ArrayList<Ammo> autoAmmoOnMap;	//automatic weapon ammo
	private float targetX = 0; private float targetY = 0;	//positions that the homing missiles should aim for
	private ArrayList<Explosion> explosionsOnMap;
	private Wave currentWave;
	private ArrayList<Zombosaur> zombosaursOnMap;

	private Player player;
	
	public Map(int level, Player player){
		mapGrid = new ArrayList<ArrayList<Tile>>();
		trail = DataManager.getTrail(level);
		
		//Creates the ArrayList of Tiles
		for(int i = 0; i < MAP_HEIGHT; i++){		//y coordinate(row)
			ArrayList<Tile> row = new ArrayList<Tile>(MAP_WIDTH);
			for(int j = 0; j < MAP_WIDTH; j++){		//x coordinate (col)
				if(isTrailTile(new GridCoordinate(j,i))) row.add(getTrailTile(new GridCoordinate(j,i)));
				else row.add(new BackgroundTile(new GridCoordinate(j,i)));
			}
			mapGrid.add(row);
		}
		
		automaticWeapons = new ArrayList<AutomaticWeapon>();
		defensiveItems = new ArrayList<DefensiveItem>();
		minesOnMap = new ArrayList<Mine>();
		ammoOnMap = new ArrayList<Ammo>();
		autoAmmoOnMap = new ArrayList<Ammo>();
		setCurrentWave(WaveFactory.getWave(0, level));
		zombosaursOnMap = new ArrayList<Zombosaur>();
		explosionsOnMap = new ArrayList<Explosion>();
		this.player = player;
		
		this.tickCount = 0;
	}
	
	public ArrayList<GridCoordinate> getTrailCoordinates(){
		ArrayList<GridCoordinate> trailCoords = new ArrayList<GridCoordinate>(this.trail.size());
		for(int i = 0; i < this.trail.size(); i++){
			trailCoords.add(i, trail.get(i).getGridCoordinates());
		}
		return trailCoords;
	}
	
	public boolean isTrailTile(GridCoordinate coordinate){
		for(int i = 0; i < trail.size(); i++){
			if(trail.get(i).getGridCoordinates().getX() == coordinate.getX() && trail.get(i).getGridCoordinates().getY() == coordinate.getY()){
				return true;
			}
		}
		return false;
	}
	
	public TrailTile getTrailTile(GridCoordinate coordinate){
		if(isTrailTile(coordinate)){
			for(int i = 0; i < trail.size(); i++){
				if(trail.get(i).getGridCoordinates().getX() == coordinate.getX() && trail.get(i).getGridCoordinates().getY() == coordinate.getY()){
					return trail.get(i);
				}
			}
		}
		return null;
	}
	
	public boolean layItem(String selectedItem, GridCoordinate coordinates){
		boolean layCompleted = false;
		if(selectedItem == "") return false;
		Item item = ItemFactory.createItemFromImageName(selectedItem, coordinates);
		if(item instanceof AutomaticWeapon){
			if(isTrailTile(coordinates)) {
				Controller.displayMessage("Automatic weapons can't be laid on the trail!",1500);
				layCompleted =  false;
			}
			else if(mapGrid.get(coordinates.getY()).get(coordinates.getX()).hasItem()){
				Controller.displayMessage("You can't lay an automatic weapon on a tile which already has an item!",1500);
				layCompleted =  false;
			}
			else{
				System.out.println("Automatic weapon layed on tile "+coordinates.getX()+" "+coordinates.getY());
				mapGrid.get(coordinates.getY()).get(coordinates.getX()).placeItem(item);
				automaticWeapons.add((AutomaticWeapon)item);
				layCompleted =  true;
			}
		} 
		else if( item instanceof DefensiveItem ){
			if(mapGrid.get(coordinates.getY()).get(coordinates.getX()).hasItem()){
				Controller.displayMessage("You can't lay a defensive item on a tile which already has an item!", 1500);
				layCompleted =  false;
			}
			else if( !isTrailTile(coordinates)){
				Controller.displayMessage("Defensive items can only be laid on the trail!",1500);
				layCompleted =  false;
			}
			else{
				if(item instanceof Mine) this.minesOnMap.add((Mine)item);
				else {
					System.out.println("Defensive Item Layed on tile ");
					defensiveItems.add((DefensiveItem)item);
				}
				mapGrid.get(coordinates.getY()).get(coordinates.getX()).placeItem(item);
				layCompleted =  true;
			}
		}
		else {
			System.out.println("This type of item is not recognized!");
		}
		return layCompleted;
	}

	public void onTick(int tickCount) {
		if((int)(Controller.ticksPerSecond*timeBeforeSpawn) == 0) timeBeforeSpawn = 0.5f;	//avoid division by zero
		if(tickCount%(int)(Controller.ticksPerSecond*timeBeforeSpawn) == 0) {
			getNextEnemyFromWave();
		}
		//check defensive items
		for(int i = 0; i < this.defensiveItems.size(); i++){
			if(defensiveItems.get(i).isDestroyed()){
				defensiveItems.remove(i);
			}
		}
		//checks the mines on the map.
		for(int i = 0; i < minesOnMap.size(); i++){
			if(minesOnMap.get(i).isDestroyed() == true){
				this.explosionsOnMap.add(new Explosion(minesOnMap.get(i).getPosition(), minesOnMap.get(i).getRange().size()));
				for(int j = 0; j < minesOnMap.get(i).getRange().size(); j++){
					TrailTile t = getTrailTile( minesOnMap.get(i).getRange().get(j));
					//if the tile is a trail tile and there is a zombo on it, hit the zombo.
					if(t != null && t.tileHasZombosaur()){ 
						ArrayList<Zombosaur> z = t.getZombosaursOnTile();
						for(int k = 0; k < z.size(); ++k){
							float vectorY = -(z.get(k).getyPos()-(minesOnMap.get(i).getPosition().getY()+0.5f));
							float vectorX = (z.get(k).getxPos()-(minesOnMap.get(i).getPosition().getX()+0.5f));
							float angle = (float)Math.atan(vectorY/vectorX);
							if(vectorX < 0) angle += Math.PI;	//atan returns an angle between -PI/2 and PI/2. This corrects for that.
							z.get(k).hit(minesOnMap.get(i).getDamage(),(int)(angle*180/Math.PI));
						}
					}
				}
				this.minesOnMap.remove(i);
			}
		}
		//move all the zombosaurs
		for(int i = 0; i < trail.size(); i++){
			if(trail.get(i).tileHasZombosaur()){
				for(int j = 0; j < trail.get(i).getZombosaursOnTile().size(); j++){
					Zombosaur zombo = trail.get(i).getZombosaursOnTile().get(j);
					
					//call step on all the zombosaurs on the tile
					boolean movedToNewTile = false;
					if(i < trail.size() - 1){
						movedToNewTile = zombo.update(trail.get(i+1));
					}
					else{
						if(zombo.getRemainingHealth() <= 0){
							zombo.deathSequence();
						}
					}
					//if the zombosaur entered a new tile, check if it needs to be rotated
					if(movedToNewTile){
						if(i < trail.size()-2) {
							//add zombosaur to the next tile
							trail.get(i+1).zombosaurEnteredTile(zombo);
							zombo.checkNextTileAndRotate(trail.get(i+2));
							//remove zombosaur from old tile
							trail.get(i).zombosaurExitedTile(zombo);
							//add zombosaur to approaching tile list for the tile after the one it was just moved to
							trail.get(i+2).zombosaurApproaching(zombo);
						}
						else{
							trail.get(trail.size() - 1).zombosaurEnteredTile(zombo);
							trail.get(trail.size() - 2).zombosaurExitedTile(zombo);
							System.out.println("Last Tile: " + zombo.getCurrentTile().getX() + ", " + zombo.getCurrentTile().getY());
							zombo.stop();
						}
					}
					
					//if the zombosaur is finished it's death sequence, we must remove it
					if(zombo.isFinishedDying()){
						zombo.getCurrentTile().zombosaurExitedTile(zombo);
						if(i < trail.size() - 1){
							trail.get(trail.indexOf(zombo.getCurrentTile()) + 1).removeApproachingZombosaur(zombo);
						}
						removeZombosaurFromMap(zombo);
						player.addPoints(KILL_ZOMBOSAUR_POINTS);
					}
				}
			}
		}
		//check if there is zombosaurs on the last trail tile. If so damage the player.
		if(trail.get(trail.size()-1).tileHasZombosaur()){
			for(int i = 0; i < trail.get(trail.size()-1).getZombosaursOnTile().size(); i++){
				Zombosaur zombo = trail.get(trail.size()-1).getZombosaursOnTile().get(i);
				player.damagePlayer(zombo.getDamage()/(float)Controller.ticksPerSecond);
				//System.out.println("Player health = "+player.getHealth());
			}
		}
		//check weapons on map's ranges for zombos
		for(int i = 0; i < automaticWeapons.size(); ++i){
			AutomaticWeapon weapon = automaticWeapons.get(i);
			//if(!weapon.isFired()){
			if(weapon.fireNew(tickCount)){
			for(int j = weapon.getRange().size() - 1; j >= 0; --j){
				TrailTile t = getTrailTile(weapon.getRange().get(j));
				if(t != null && ( t.tileHasZombosaur() || t.tileHasApproachingZombosaur() ) && !t.hasItem()){
					int x = t.getGridCoordinates().getX();
					int y = t.getGridCoordinates().getY();
					int dX = t.getGridCoordinates().getX() - weapon.getPosition().getX();
					int dY = t.getGridCoordinates().getY() - weapon.getPosition().getY();
					int range = weapon.getRangeInt();
					
					
					//We want the weapons to fire ammo to the end of their ranges so we have to
					//check where they are shooting and then if that isn't the end of the range
					//then set the final position to the end of the range.
					if( Math.abs(dX) < weapon.getRangeInt() && Math.abs(dY) < weapon.getRangeInt() ){

						if(dX > 0){
							x = weapon.getPosition().getX() + range;
						}
						else if(dX < 0){
							x = weapon.getPosition().getX() - range;
						}
						else{
							x = weapon.getPosition().getX();
						}

						if(dY > 0){
							y = weapon.getPosition().getY() + range;
						}
						else if (dY < 0){
							y = weapon.getPosition().getY() - range;
						}
						else{
							y = weapon.getPosition().getY();
						}
					}
					System.out.println("X = " + x + " y = " + y);
					fireAutoWeapon(weapon, new GridCoordinate(x, y), tickCount );
					break;
				}
			}
			}
		}
		//updates manual ammo position on map
		for(int i = 0; i < ammoOnMap.size(); ++i){
			Ammo ammo = ammoOnMap.get(i);
			if(ammo.getWeapon() instanceof ManualWeapon){
				int finalX = (int)ammo.getVelocity().getX2() ;
				int finalY = (int)ammo.getVelocity().getY2() ;
				float curX = (int)ammo.getX();
				float curY = (int)ammo.getY();
				if( curX == finalX && curY == finalY){
					this.explosionsOnMap.add(new Explosion(new GridCoordinate(finalX, finalY), ammo.getWeapon().getRange().size()));
					for(int j = 0; j < ammo.getWeapon().getRange().size(); j++){
						TrailTile t = getTrailTile( ammo.getWeapon().getRange().get(j));
						//if the tile is a trail tile and there is a zombo on it, hit the zombo.
						if(t != null && t.tileHasZombosaur()){ 
							ArrayList<Zombosaur> z = t.getZombosaursOnTile();
							for(int k = 0; k < z.size(); ++k){
								float vectorY = -(z.get(k).getyPos()-(finalY+0.5f));
								float vectorX = (z.get(k).getxPos()-(finalX+0.5f));
								float angle = (float)Math.atan(vectorY/vectorX);
								if(vectorX < 0) angle += Math.PI;	//atan returns an angle between -PI/2 and PI/2. This corrects for that.
								z.get(k).hit(ammo.getWeapon().getDamage(),(int)(angle*180/Math.PI));
							}
						}
					}
					removeAmmoFromMap(ammo);
				}
				else{
					ammo.update();
				}
			}
			if(ammo instanceof HomingMissileAmmo){
				((HomingMissileAmmo) ammo).updateDirection(targetX, targetY);
				TrailTile t = getTrailTile( new GridCoordinate((int)ammo.getCenterX(), (int)ammo.getCenterY()));
				if(checkIfZombosaurHit(t, ammo)){
					((HomingMissileAmmo) ammo).decrementZombosaurCount();
					if(((HomingMissileAmmo) ammo).getRemainingZombosaurs() <= 0) removeAmmoFromMap(ammo);
				}
			}
		}
		for( int i = 0; i < autoAmmoOnMap.size(); ++i){
			boolean hit = false;
			AutomaticWeapon autoWeap = (AutomaticWeapon)autoAmmoOnMap.get(i).getWeapon();
			if( autoWeap != null ){
				TrailTile t = getTrailTile( new GridCoordinate((int)autoAmmoOnMap.get(i).getCenterX(), (int)autoAmmoOnMap.get(i).getCenterY() ));
				if(checkIfZombosaurHit(t, autoAmmoOnMap.get(i))){
					removeAutoAmmoFromMap(autoAmmoOnMap.get(i));
					hit = true;
					break;
				}
				if( hit){
					continue;
				}
				else if(autoAmmoOnMap.get(i).getVelocity().getX2() == autoAmmoOnMap.get(i).getX() && autoAmmoOnMap.get(i).getVelocity().getY2() == autoAmmoOnMap.get(i).getY()){
					removeAutoAmmoFromMap(autoAmmoOnMap.get(i));
				}
				else{
					autoAmmoOnMap.get(i).update();
				}
			}
		}
		//updates explosions on map
		for(int i = 0; i < explosionsOnMap.size(); ++i){
			Explosion exp = explosionsOnMap.get(i);
			exp.incrementTickCount();
			if(exp.explosionFinished()) explosionsOnMap.remove(exp);
		}
		if(this.player.getHealth() <= 0.0f){
			Controller.displayMessage(MessageFactory.GAME_OVER_MESSAGE, 4000);
			Timer gameOverTimer = new Timer(4000, new ActionListener(){
				@Override
				public void actionPerformed(ActionEvent arg0) {
					Controller.returnToMainMenu();
				}
			});
			Controller.stopTimer();
			gameOverTimer.setRepeats(false);
			gameOverTimer.start();
		}
		//check if all the zombosaurs are dead
		else if((this.currentWave.size() == 0 && this.zombosaursOnMap.size() == 0)){
			//System.out.println("Advancing state because zombosaurs are dead");
			Controller.advanceState();
			for(int i = 0; i < automaticWeapons.size(); ++i){
				automaticWeapons.get(i).setLastFiredTick(0);
			}
		}
	}
	
	/**
	 * Fires a manual weapon from the spaceship to the position specified
	 * by endPos
	 * 
	 * @param weapon
	 * @param endPos
	 */
	public void fireManualWeapon(ManualWeapon weapon, GridCoordinate endPos){
		Ammo ammo = player.getCargo().removeAmmo(weapon);
		//(14,5) is the position of the spaceship
		ammo.setX(14);
		ammo.setY(5);
		//Velocity of the ammo must be set prior to firing
		ammo.setVelocity(new Velocity(14, 5, endPos.getX(), endPos.getY(), weapon.getSpeed()));
		//The final position must be set for the range to be created
		weapon.setPosition(endPos);
		weapon.createRange();
		this.ammoOnMap.add(ammo);
	}
	
	/**
	 * Fires an automatic weapon from the spaceship to the position specified
	 * by endPos
	 * 
	 * @param weapon
	 * @param endPos
	 */
	public void fireAutoWeapon(AutomaticWeapon weapon, GridCoordinate endPos, int curTick){
		if(weapon.fireNew(curTick)){
			Velocity vel = new Velocity(weapon.getPosition().getX(), weapon.getPosition().getY(), endPos.getX(), endPos.getY(), weapon.getSpeed() );
			Ammo ammo = new Ammo(vel, weapon.getPosition().getX(), weapon.getPosition().getY(), weapon);
			this.autoAmmoOnMap.add(ammo);
			//weapon.setFired(true);
			weapon.setLastFiredTick(curTick);
		}
	}
	
	public boolean checkIfZombosaurHit(TrailTile t, Ammo ammo){
		if(t != null){
			//check through all zombos on map to see if it is "in contact" with the ammo
			for(int j = 0; j < this.getZombosaursOnMap().size(); j++){
				//Zombosaur zombo = t.getZombosaursOnTile().get(j);
				Zombosaur zombo = this.getZombosaursOnMap().get(j);
				float cenX = ammo.getCenterX();
				float cenY = ammo.getCenterY();
				float zomX = zombo.getxPos();
				float zomY = zombo.getyPos();
				
				if( zomX > cenX - 0.3 && zomX < cenX + 0.3 && zomY > cenY - 0.3 && zomY < cenY + 0.3){
					//zombo.hit(ammo.getWeapon().getDamage());
					ammo.getWeapon().hit(zombo, (int)(ammo.getVelocity().getAngle()*180/Math.PI));
					return true;
				}
			}
		}
		return false;
	}
	
	public int getAutomaticWeaponCount(){
		return this.automaticWeapons.size();
	}
	
	public ArrayList<AutomaticWeapon> getWeaponsOnMap(){
		return this.automaticWeapons;
	}
	
	public int getDefensiveItemCount(){
		return this.defensiveItems.size();
	}
	
	public List<DefensiveItem> getDefensiveItemOnMap(){
		return this.defensiveItems;
	}
	
	public Wave getCurrentWave() {
		return currentWave;
	}

	public void setCurrentWave(Wave currentWave) {
		this.currentWave = currentWave;
		this.timeBeforeSpawn = 2.0f;	//reset the time before the first enemy spawns to default
	}
	
	public int getTickCount() {
		return tickCount;
	}

	public void setTickCount(int tickCount) {
		this.tickCount = tickCount;
	}
	
	public static ArrayList<ArrayList<Tile>> getMapGrid() {
		return mapGrid;
	}

	public static void setMapGrid(ArrayList<ArrayList<Tile>> mapGrid) {
		Map.mapGrid = mapGrid;
	}
	
	public static int getMapWidth() {
		return MAP_WIDTH;
	}

	public static int getMapHeight() {
		return MAP_HEIGHT;
	}
	
	public void addAmmoToMap(Ammo ammo){
		this.ammoOnMap.add(ammo);
	}
	
	public void removeAmmoFromMap(Ammo ammo){
		this.ammoOnMap.remove(ammo);
	}
	
	public ArrayList<Zombosaur> getZombosaursOnMap() {
		return zombosaursOnMap;
	}
	
	public ArrayList<Ammo> getAmmoOnMap() {
		return ammoOnMap;
	}

	public void setAmmoOnMap(ArrayList<Ammo> ammoOnMap) {
		this.ammoOnMap = ammoOnMap;
	}
	
	public ArrayList<Ammo> getAutoAmmoOnMap() {
		return autoAmmoOnMap;
	}
	
	public void removeAutoAmmoFromMap(Ammo ammo){
		this.autoAmmoOnMap.remove(ammo);
	}
	
	public void addAutoAmmoToMap(Ammo ammo){
		this.autoAmmoOnMap.add(ammo);
	}
	
	public ArrayList<Explosion> getExplosionsOnMap() {
		return explosionsOnMap;
	}
	
	public ArrayList<Mine> getMinesOnMap() {
		return minesOnMap;
	}

	public void setMinesOnMap(ArrayList<Mine> minesOnMap) {
		this.minesOnMap = minesOnMap;
	}

	public void removeZombosaurFromMap(Zombosaur zombo){
		zombosaursOnMap.remove(zombo);
	}

	public float getTargetX() {
		return targetX;
	}

	public void setTargetX(float targetX) {
		this.targetX = targetX;
	}

	public float getTargetY() {
		return targetY;
	}

	public void setTargetY(float targetY) {
		this.targetY = targetY;
	}

	
	/**
	 * If there are any enemies left on the current wave stack,
	 * this pops the top one off and adds it to the zombosaursOnMap array.
	 * 
	 * returns true if a zombosaur is added to the map, false if the stack is empty
	 */
	private boolean getNextEnemyFromWave(){
		if(this.currentWave.size() > 0){
			WaveEntity entity = this.currentWave.pop();
			Zombosaur zombo = entity.getZombosaur();
			trail.get(0).zombosaurEnteredTile(zombo);
			this.zombosaursOnMap.add(zombo);
			this.timeBeforeSpawn = entity.getDelay();
			return true;
		}
		return false;
	}
}
